package com.adee.serializer.kryo;

import com.alibaba.fastjson2.JSON;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Kryo序列化框架
 *
 * Kryo使用指南：https://blog.csdn.net/xiaomin1991222/article/details/84913082
 *
 * Kryo一个快速有效的Java二进制序列化框架，它依赖底层ASM库用于字节码生成，因此有比较好的运行速度。
 * Kryo的目标就是提供一个序列化速度快、结果体积小、API简单易用的序列化框架。Kryo支持自动深/浅拷贝，
 * 它是直接通过对象->对象的深度拷贝，而不是对象->字节->对象的过程。
 *
 * Kryo相较于其他序列化框架性能最优
 *
 *
 *
 * 需要注意的是使用Output.writeXxx时候一定要用对应的Input.readxxx，比如Output.writeClassAndObject()
 * 要与Input.readClassAndObject()。
 *
 * 通用性
 *
 * 首先Kryo官网说自己是一款Java二进制序列化框架，其次在网上搜了一遍没有看到Kryo的跨语言使用，只是一些文章提及了
 * 跨语言使用非常复杂，但是没有找到其它语言的相关实现。
 *
 * 易用性
 *
 * 在使用方式上Kryo提供的API也是非常简洁易用，Input和Output封装了你几乎能够想到的所有流操作。Kryo提供了丰富的
 * 灵活配置，比如自定义序列化器、设置默认序列化器等等，这些配置使用起来还是比较费劲的。
 *
 * 可扩展性
 *
 * Kryo默认序列化器FiledSerializer是不支持字段扩展的，如果想要使用扩展序列化器则需要配置其它默认序列化器。
 *
 * 比如：
 *
 * private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
 *     Kryo kryo = new Kryo();
 *     kryo.setRegistrationRequired(false);
 *     kryo.setDefaultSerializer(TaggedFieldSerializer.class);
 *     return kryo;
 * });
 * 性能
 *
 * 使用Kryo测试上面的测试用例，Kryo序列化后的字节大小为172 ，和FST未经优化的大小一致。时间开销如下：
 *
 *
 *
 * 我们同样关闭循环引用配置和预注册序列化类，序列化后的字节大小为120，因为这时候类序列化的标识是使用的数字，而不是类全名。
 * 使用的是时间开销如下：
 *
 *
 *
 * 数据类型和语法结构支持性
 *
 * Kryo对于序列化类的基本要求就是需要含有无参构造函数，因为反序列化过程中需要使用无参构造函数创建对象。
 *
 * 5. Kryo 的优点
 * 高性能：Kryo 采用二进制格式进行序列化，相比 Java 自带的序列化机制，可以显著降低序列化和反序列化的时间开销。
 * 小尺寸：Kryo 生成的序列化结果通常比 Java 自带的序列化结果更小，减少了网络传输的负载。
 * 可扩展性：Kryo 支持自定义序列化器和注册机制，可以方便地处理复杂对象和不同版本之间的兼容性问题。
 * 易于使用：Kryo 提供简单易用的 API，只需几行代码即可完成序列化和反序列化操作。
 *
 * 6. Kryo 的缺点
 * 不跨语言：Kryo 是一个专门为 Java 设计的序列化框架，不支持与其他编程语言进行互操作。
 * 版本兼容性：由于 Kryo 对类结构信息进行了压缩和优化，因此在更新类定义时可能会导致反序列化失败。需要注意保持类的版本兼容性。
 *
 * 7. Kryo 的使用注意事项
 * 注册类：在使用 Kryo 进行序列化和反序列化之前，需要先注册需要序列化的类。可以通过kryo.register(Class)方法来实现。
 * 线程安全：Kryo 实例本身不是线程安全的，因此在多线程环境下应该避免共享同一个 Kryo 实例。
 * 缓存大小：Kryo 使用缓存来存储已经序列化或反序列化过的对象，默认情况下缓存大小为 4096。如果需要处理大量的对象，请适当增加缓存大小以提高性能。
 *
 */
public class TestKryo {

    public static void main(String[] args) {
        Map<String,Object> hobbies = new HashMap<>();
        hobbies.put("游泳", "蛙泳自学中...");
        hobbies.put("跑步", "跑一休一");

        Student student = new Student.StudentBuilder()
                .id(1)
                .name("张三")
                .teachers(Arrays.asList(new Teacher(1,"王老师"),new Teacher(5, "陈老师")))
                .hobbies(hobbies)
                .build();

        System.out.println("序列化之前：");
        System.out.println(JSON.toJSONString(student));
        byte[] bytes = encoder(student);

        System.out.println("反序列化之后：");
        Student student2 = decoder(bytes, Student.class);
        System.out.println(JSON.toJSONString(student2));
    }

    // 使用ThreadLocal的作用就是防止多线程调用相同对象时带来的性能瓶颈
    private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
        // Kryo对象是非线程安全的，使用时，为了提高性能，需要结合ThreadLocal使用
        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);//不要提前预注册类，防止不同jvm上的类序列号不一致，导致反序列化失败
        return kryo;
    });

    // 序列化
    public static byte[] encoder(Object object) {
        Output output = new Output();
        kryoLocal.get().writeObject(output,object);
        output.flush();
        return output.toBytes();
    }

    // 反序列化
    public static <T> T decoder(byte[] bytes, Class<T> tClass) {
        Input input = new Input(bytes);
        Object ob = kryoLocal.get().readClassAndObject(input);
        return (T) ob;
    }


    @Data
    @Accessors(chain = true)
    @Builder
    public static class Student implements Serializable {

        /**
         * serialVersionUID的作用？
         * 答：serialVersionUID 是Java序列化机制中使用的一个版本号，用来控制对象反序列化时的兼容性。
         * 在Java中，当一个对象被序列化后，如果在序列化和反序列化之间，对该对象的结构进行了修改，那么
         * 反序列化就会失败，这时候就需要通过指定 serialVersionUID 来指定兼容版本。 序列化机制会使用
         * 类的serialVersionUID来验证版本一致性。在进行反序列化时，JVM会把传来的字节流中的serialVersionUID
         * 与本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的，可以进行反序列化，否则就会出现
         * 不兼容版本异常。 如果一个类没有定义 serialVersionUID ，那么在程序中修改了该类的结构后，就会导致
         * 反序列化失败。为了解决这个问题，可以在类中定义一个固定的 serialVersionUID 值。在序列化和反序列化
         * 的时候，JVM会自动识别该值，并进行版本的比较，从而确保只有同一版本的类才能进行反序列化。因此，为了保证
         * 序列化和反序列化的兼容性，一般建议在所有需要序列化的类中定义 serialVersionUID 值。
         *
         */
        public static final long serialVersionUID = 2L;

        private long id;
        private String name;

        private List<Teacher> teachers;

        private Map<String,Object> hobbies;
    }

    @AllArgsConstructor
    public static class Teacher implements Serializable {
        public static final long serialVersionUID = 1L;

        private long id;
        private String name;
    }
}
